spb/tools-spboucher-ai
Public
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { getCollection, listCollections } from "@/lib/public/queries";5import { Breadcrumbs } from "@/components/breadcrumbs";6import { ToolCard } from "@/components/tool-card";7import { fmtDate } from "@/lib/utils";8export const revalidate = 600;9export async function generateStaticParams() { try { return (await listCollections()).map((c) => ({ slug: c.slug })); } catch { return []; } }10export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {11 const c = await getCollection((await params).slug); if (!c) return { title: "Not found" };12 return { title: c.title, description: c.description.slice(0, 160), alternates: { canonical: `/collections/${c.slug}` } };13}14export default async function CollectionPage({ params }: { params: Promise<{ slug: string }> }) {15 const c = await getCollection((await params).slug); if (!c) notFound();16 return (17 <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6">18 <Breadcrumbs items={[{ href: "/collections", label: "Collections" }, { label: c.title }]} />19 <h1 className="display h1 mt-3">{c.title}</h1>20 <p className="prose-tool mt-3 max-w-3xl">{c.description}</p>21 <p className="mt-2 text-xs text-fg-3">{c.items.length} tools · updated {fmtDate(c.updatedAt)} · <Link href={`/compare?tools=${c.items.slice(0, 4).map((i) => i.slug).join(",")}`} rel="nofollow" className="text-accent hover:underline">compare the first four</Link></p>22 <h2 className="sr-only">Tools in this collection</h2>23 <ol className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{c.items.map((t, i) => <li key={t.id} className="relative"><span className="absolute -left-1 -top-2 z-10 grid h-6 w-6 place-items-center rounded-full bg-accent text-xs font-semibold text-accent-fg" aria-hidden>{i + 1}</span><ToolCard t={t} blurb={t.blurb} className="h-full" /></li>)}</ol>24 </div>25 );26}27